feat(pr-workflow): add the /mms-evidence orchestrator, runners, and publish gate - #84
feat(pr-workflow): add the /mms-evidence orchestrator, runners, and publish gate#84MajorLift wants to merge 72 commits into
/mms-evidence orchestrator, runners, and publish gate#84Conversation
pr-validate and falsifying-test skills
Context budgetWhat this PR costs an agent, measured from an install rather than read from the diff. Three tiers, and only the first is unavoidable.
Frontmatter is the only tier paid unconditionally — every agent loads it on every run once the skill is installed, used or not, because it is what the agent reads to decide relevance. The 28 skills across the eleven open skill PRs sit at a median of ~1,716 tokens selected and ~1,860 with references followed. All are within the 1,536-character description budget. Selected is paid only when the agent picks the skill. + refs & knowledge is the ceiling if every bundled reference is then read; it is a worst case, not an expectation. Method
These figures are pinned to the commit above and drift on every push; #96 tracks automating them. |
pr-validate and falsifying-test skillsevidence and falsifying-test skills
evidence and falsifying-test skills/mms-evidence orchestrator and falsifying-test skill
Each encodes a failure that actually shipped, rather than a principle that sounded right. `unmeasured-join` targets the costliest one — a conclusion assembled from true facts through a step nobody measured, which survives review because the facts check out and the join reads as prose. `instrument-check` requires a positive and a negative control before a measurement counts, after a mutation runner reported a falsification for a line it never wrote. `scope-of-search` makes a negative carry the pattern that produced it, after a grep keyed on a variable name declared absent what its own output printed two blocks later. `coverage-partition` replaces "the test has power" with which cases guard which mechanism. `selection-audit` separates a count from the rule that produced it, after two different selections landed on the same number and the match closed the question. `distinguishing-observation` and `observability-gap` point the same discipline at debugging: design the observation that separates candidates rather than confirms the favourite, and establish what signal exists on a path before reading more of it. The `evidence` links are forward references — that skill ships in #84 and is not on main yet.
/mms-evidence orchestrator and falsifying-test skill/mms-evidence orchestrator and red-on-base skill
Its substance is the same as everything else here: what counts as proof, and how a proof can look right while testing the wrong thing. Its falsifier — a base commit that fails for the wrong reason produces an identical exit code and proves nothing — is the same move `unmeasured-join` and `scope-of-search` make on different material. It sat in #84 because it was written alongside the orchestrator, before the instrument and the reasoning that points it were separate ideas. Every other engine already ships in its own pull request. Renamed from `falsifying-test` before the move, because it and `falsify-probe.sh` shared a root while answering different questions — base-against-branch asks whether a test is connected to the reported bug, one-commit-with-a-mutated-line asks whether a suite notices the mechanism going away.
Step 6 asked for both runs shown and left the operator to produce them, which is the shape that yields a retyped paste indistinguishable from output nobody ran. The run workflow already takes `ref` and `baseline`, executes the same command at both commits, and attaches the artifacts to a URL a reader can open — so the step names it. The larger risk was confusion with `falsify-probe.sh`, which shares the two-arm shape and answers a different question: base-against-branch asks whether a test is connected to the reported bug, one-commit-with-a-mutated-line asks whether it notices the mechanism going away. A test can pass either and fail the other. The skill now states the distinction in a table rather than leaving two similarly named things to be conflated, and notes that the runner's guards mechanise this skill's own falsifier — a red arm that ran fewer tests, or failed to load, is refused rather than counted.
Two things in this PR shared a root and a two-arm shape while answering different questions: the skill compares a base commit against a branch to show a test is connected to the reported bug, and `falsify-probe.sh` mutates one line at one commit to show a suite notices the mechanism going away. A test can satisfy either and fail the other, so the names had to stop rhyming. `red-on-base` names the skill's own discipline — the test must be red on the base commit — and shares no root with the runner. The evidence category keeps its name: a falsifying regression test is the artifact, and this is the procedure that produces one. The runner's stderr described its own result as "a falsifying test", which was the same collision inside the output of the thing causing it; it now says what it proves and points at the other experiment by name.
Every other engine `evidence` names ships in its own pull request — `memory-leak`, `race-condition-repro`, `supply-chain-audit`, `react-render-delta`, `agent-run-cost`. This one was the sole exception, and the reason was authoring order rather than design: it was written alongside the orchestrator before the split between the instrument and the reasoning that points it existed as a concept. It lands in #106 with the other reasoning skills, whose substance is the same kind — what counts as proof, and how a proof can look right while testing the wrong thing. What stays here is the machinery: the runners, the run workflow, the gate, the hooks. The B3 engine cell now names a skill that arrives in #106, which is a dangling name in a table rather than a broken link, and resolves whichever order the two merge.
Eleven checks tested properties of the comment text. Text can be perfect and land somewhere nobody will read it, and that is what happened: across one register of published runs, 22 of 27 comments went onto pull requests that had already merged when they were posted — median 22 days after the merge, one 178 days after. The gate passed every one, because no property of a comment reveals the state of its destination. Check 12 takes `--target owner/repo#N` and blocks on anything that is not open. Omitting the target fails rather than passes: an unchecked destination is the condition that produced all 22. Without `gh` on PATH it reports UNVERIFIED and still refuses, since the point is that silence here is indistinguishable from success.
The CI workflow sources runners by `skills_ref`, and every run this week pointed at a fork branch. Four fixes were made there and never reached this one: the mutation now travels through `ENVIRON` instead of an escape-processed `awk -v` assignment, the artifact reports the line read back off disk beside the line requested, `--expect-fail` turns a red arm in the wrong place into its own outcome, and `--metric` lets the caller name what a probe counted. So the defect this PR's own description cites as the reason instruments must report their effect was, until now, still live in the instrument this PR ships. The probe comes across too, with the import fix that made it resolve at the destination the workflow copies it to. Two copies of the same scripts on two branches, edited in both directions — `attest-gate.sh` had a check the fork lacked, so it stays as it is here.
Check 9 is called "verdict matches artifact" and compares verdict words. Nothing compared the numbers, and prose drifting from the exhibit beside it is the most common way one of these goes wrong. Found by building a demonstration artifact to test this gate: the prose read "0 errors over 48 skills" directly above an exhibit reading "47 skill(s) checked", and named a warning class with zero instances in the output it was describing. Every other check passed. Two independent readers caught both, which is the argument for moving it into the layer that always runs rather than the one that costs money and sometimes never reports. Narrow on purpose, because a noisy check is an ignored one. Two-plus digits only, and only those absent from every fenced block; whole URLs, issue refs, versions, dates, SHAs, file:line citations, hyphenated identifiers and regex quantifiers are excluded as references rather than measurements. Each exclusion was added after a control run flagged something that was not a figure. Across eight real artifacts it flags one, correctly: a verdict line quoting an author's "730 tests" beside an exhibit measuring 731, where nothing distinguishes the cited figure from the measured one.
With `gh` absent the check printed UNVERIFIED and exited 0, so on a machine without it — running locally — the destination check announced that it had not run and the gate reported clean. A control that cannot run is indistinguishable from one that passed unless the exit code says otherwise.
Renamed on the security-domain branch; installs as `mms-lavamoat-policy`.
A diligence comment renders no verdict and deliberately does not use the Validation Run envelope. That exemption meant it was checked by nothing: this gate only knew the Validation Run shape, and `pr-evidence-gate.py` by design does not trip on a body claiming no verdict. So every rule the diligence skills state about their own output — including "runtime claims need a runtime artifact" — had no execution path. It showed. A lavamoat comment shipped with no marker pair, an `npm pack` specifier set no reader could fetch, and two bare integers traceable to nothing. `--diligence` swaps the four envelope checks for that contract's own — its marker pair, its header, permalinks pinned to a tag or SHA rather than a branch head, and a runtime claim check asking for the thing a `/blob/` link cannot witness. 3, 8 and 9 report SKIP with the reason rather than passing silently, since a check that cannot fail should not read as a check that passed. Everything downstream of the envelope is shared, because those defects are shared. Run against the comment that prompted this, it fails 1, 5 and 13 and passes the rest.
This repository is public. Naming a private repository here discloses its existence,
its owner and roughly its contents to every reader — and a prohibition naming it
("do not re-host to X, it is private") discloses exactly as much as a recommendation
would. Four such references were doing that, and the guidance survives without them:
the rule is audience-reachability, which is stated directly rather than by example.
Two memory-file citations offered as "source of truth" pointed into a private repo, so
a reader was told to follow a rule whose justification they cannot open. The reasoning
is inlined; the pointer is gone.
The publish-surface snippet hardcoded a GitHub username, which decided the destination
for whoever ran it. Now derived from `gh api user --jq .login`, and the surrounding
prose is second-person rather than first — a shared skill has no "my PRs".
`/attest` is no longer linked to a personal repository. That leaves it named but not
resolvable, which is honest and is the smaller problem; the workflow depending on a
command nobody else has is tracked separately.
The re-hosting section named a personal S3 bucket, its region, its prefix layout, and stated that anonymous `GetObject` is enabled under `public/*`. That is a live unauthenticated endpoint advertised, with its structure, to every reader of a public repository — a larger disclosure than the repository names removed alongside it, and one that reads as configuration rather than as a secret, which is why it survived two passes over this file. Now `EVIDENCE_BUCKET` and `EVIDENCE_REGION` from the environment. The requirements the bucket must satisfy — anonymous GetObject under `public/*`, listing disabled — are stated, because those are the load-bearing part; the name never was. The G5 lane likewise named a private test fork, which carried both the org and a personal handle. Now "your own test fork".
…setup Removing the hardcoded username left `gh pr view --jq --arg me "$ME"`, which is not a thing gh supports — its built-in filter takes no --arg and the command dies with "accepts at most 1 arg(s)". Piped to real jq instead, and checked against both branches of the logic: a PR authored by someone else resolves to "skip", one authored by the caller to "body". Replacing the named bucket with `EVIDENCE_BUCKET` removed a working default and put nothing in its place, so the section told you to configure a bucket without saying what "conforming" meant. The policy is now stated: anonymous `s3:GetObject` under `public/*`, public-access blocks off for that bucket, `s3:ListBucket` to nobody. With a note that an org-owned bucket beats a personal one, since artifact links outlive their publisher.
A results section reached a public PR whose entire content was hand-typed to look like
terminal output. Three independent things had to hold for that, and all three did.
The gate is model-invoked, so it can be skipped: the publish and the gate ran as two
statements rather than one chain, and the verdict was read after the write.
The hook that fires on the publish call carried a SECOND, narrower copy of the rules —
keyed on verdict tokens — so a comment rendering no verdict satisfied neither copy.
Two rule sets means the weaker one governs whatever falls between them. The hook now
delegates to `attest-gate.sh`: one rule set, invoked by construction rather than by
choice, in the mode the body's markers imply. It fails CLOSED once it has identified a
body it is about to publish — an enforcement point that waves things through when it
cannot find its rules is not one.
And check 5 in `--diligence` had been rewritten as a phrase denylist ("npm pack",
"complete specifier set"), which is precisely the regression its own comment records as
having shipped four times: every property of plaintext is forgeable by whatever emits
the plaintext. It is a medium test again — if the artifact shows a command or a run
result, it owes the reader something fetchable. `/blob/` links are excluded, because a
permalink to a `.json` file satisfied a naive extension test and was the specific reason
the hand-typed section passed.
Comment-update URLs carry the comment id, not the issue's, so check 12 was asking
whether pull #5177261620 was open. Resolved through the API instead.
Four-arm verified: blocks the exact command and body that shipped; ignores `ls`; ignores
a `gh` read with no body write; refuses when the gate is unreachable.
`mms-evidence` named `/attest` as the gate its output passes and shipped no such thing. Anyone installing the skill got phase 0 as a script and a reference to a command only its author had, which is half a publish path for everyone else. Phase 0 is `attest-gate.sh`, already here. Phase 1 is three briefs sent to fresh instances — frame, coverage, and how it reads to a stranger — written out in `references/dispatched-passes.md` so dispatch does not depend on commands that live elsewhere. The check table in `references/phase-0-checks.md` is generated from the gate rather than retyped, so it cannot drift from what runs. Two things the port makes explicit that the original left to discipline. The gate must be the same shell chain as the publish, because running both and reading the verdict after the write is how a blocked artifact reached a public PR. And softening a check to fit the case in hand is called out as an anti-pattern: if the new version could be satisfied by better prose alone, it is no longer the check.
Three copies of this hook were on one machine and the oldest was the one wired into settings — no `gh api` matcher, no delegation to `attest-gate.sh`. Every publish through that path went ungated, and nothing noticed, because a gate that blocks nothing looks exactly like a gate with nothing to block. `gate-controls.sh` is the thing that would have noticed. Six arms: three publish routes that must block, three inputs that must pass. It copies the hook somewhere with no sibling `scripts/` so `_find_gate()` resolves the way it does in production rather than the way it does in a checkout — the difference matters, and testing the checkout copy is how the deployed one stayed broken. Both halves earned their place immediately. The negative arm caught that delegating to attest-gate ran it over EVERY published body, so an ordinary reply was judged as a failed validation run; wiring that would have blocked every normal comment. The gate now applies only to bodies carrying an artifact marker or a verdict line. And the positive arm caught itself: the enrichment probe was a single sentence, which that rule correctly ignores, so the arm had been passing because attest-gate blocked the body for an unrelated reason. A probe that fires for the wrong reason reports a working rule. The `enrichment` class is ported forward from the older copy, with the constant it depends on — it existed in the deployed version and in neither newer one.
The six arms prove the script blocks what it should. They say nothing about whether anything calls it, and those are different questions — a hook that is unwired, or wired to a path that no longer exists, is indistinguishable from a hook with nothing to block. One session ran start to finish with every PreToolUse hook inert: 306 certification markers written, none enforcing anything, and the ritual read as compliance. The check enumerates config roots rather than trusting `$HOME`. Its first version did trust it, found one settings file, reported it as "the" wiring and never looked at the second — because `$HOME` here points at a per-account directory rather than the login home. That is the same defect one level up, caught only because two configs were known to exist and one was missing from the output. It stops short of claiming liveness, and says so: a settings file naming an existing file is not proof the running session loaded it. Only a command the gate must block, issued in a session and observed to be blocked, shows that.
This is the bypass. The hook reads the command as text, so `--body-file $DIR/comment.md` resolves to nothing — and the code called that "can't read it -> nothing to scan" and allowed the write. Every publish in one long session used a shell variable for the path, so every one of them went ungated, including a comment whose entire results section was hand-typed to look like terminal output. That same body, passed by literal path, is blocked. An unreadable body is not an absent risk. By that point the command is already identified as an outward-facing write; not knowing what it carries is the reason to stop. Also rejects a body argument carrying `$` or a backtick — `--body "$(cat f)"` extracts the literal characters, scans clean, and publishes whatever the shell substitutes afterwards. The check reads the command ARGUMENT, not the body. A first version scanned body text for shell metacharacters and blocked every evidence comment ever written, because markdown inline code is backticks and these artifacts are full of them. Caught by the negative arm, which is the half of a control matrix that earns its place on days like this. Three arms added, verified end to end: the command that slipped through minutes earlier is now stopped by the deployed hook.
This branch adds `hooks/pr-evidence-gate.py` and `hooks/gate-controls.sh`, and on main neither would have survived: `hooks` is absent from `BUNDLE_DIRS`, so the linter rejects the directory outright, and absent from the bundle loop in `tools/install`, so nothing would copy it even past lint. The gate would have merged into the repository and installed to nobody. `hooks` now appears in both. The third change is the one worth keeping. The two lists are a single contract written twice, in JavaScript and in Bash, and the schema's own comment asked humans to keep them in sync by hand. `tools/check-bundle-contract.sh` asserts it instead. It builds a fixture skill carrying one file in every declared bundle directory, runs a dry-run install from a copy of the installer placed inside that tree, and checks each directory is reported as copied. It watches behaviour rather than reading the loop, because a regex over the source is the same class of evidence as the defect: two declarations agreeing with each other and neither of them being what runs. Controlled both ways. It passes as committed. Removing `hooks` from the loop makes it name `hooks` and exit 1; removing three makes it name three. Also relocates `probes/metametrics-context.test.tsx`, which lint rejected as an unexpected directory, to `assets/` where a copy-out template belongs, and points the C4 lane at it. It had been shipping with nothing in the skill naming it, which is the same defect one layer over: present, and unreachable.
d3d153d to
edf65a3
Compare
A check nobody invokes is a check that fires once, on the day it was written. This one guards a contract that drifts silently: a bundle directory declared in `BUNDLE_DIRS` and missing from the install loop reaches no consuming repo, and nothing errors, because the skill still installs, still loads, and still answers without whatever that directory held. It runs in the lint job, after `yarn lint` and before the changelog steps. It leaves no files behind, so the job's clean-working-directory gate is unaffected.
The ARTIFACT and OBS_ARTIFACT classes matched `https?://\S+` and a bare filename pattern, so any URL-shaped string discharged them. The same generator writes the claim and the string that satisfies the check, which makes presence carry no information about whether the evidence exists. Measured against the unpatched hook, with the claim and the artifact in the same block (proximity matters — the checks are unit-scoped): https://example.invalid/nope/capture.png ALLOWED `src/totally/made-up.test.ts:42` ALLOWED `evidence/never-captured.png` ALLOWED An artifact now counts only if the author could not have authored its bytes: a namespace where CI, the upload endpoint or an observability backend writes them, or a local path that is actually on disk. github.com is author-writable in general, so only /actions/runs/, /user-attachments/, /blob/, /commit/ and /pull/ under it qualify. Controls, before -> after: fabricated (want blocked) 1/3 -> 3/3 genuine (want allowed) 2/2 -> 2/2 Replaying real published bodies found the interesting case: evidence hosted in an author-controlled S3 bucket is now refused, correctly — fetching it unauthenticated proves it is fetchable, not that the author did not write it. Rather than decide that silently, EVIDENCE_GATE_ARTIFACT_HOSTS registers such hosts explicitly. Doing so is a visible downgrade from independent to merely fetchable, which is the point: it should be a choice someone made, not a property of the regex. strict default -> those bodies block bucket registered -> those bodies pass, fabrications still block The rule is presence stays necessary and stops being sufficient. Note this is still not resolution: nothing yet fetches the URL and checks for a 200. An allowlisted-but-nonexistent /actions/runs/99999999999 link is refused by the separate CI-restatement rule rather than by this one.
The hook already ran attest-gate.sh in-process on the exact bytes about to be
published, which is a stronger binding than recording a verdict against a
hash — there is no window in which the artifact can be edited after the gate
passes. Two defects meant it often did not run, or ran the wrong gate.
1. _is_evidence_artifact matched `^\*\*Verdict:\*\*`, so where the bold
stopped decided whether thirteen checks ran:
**Verdict:** proven -> all 13 checks
**Verdict: proven** -> none
The same sentence, rendered identically, one of them silently unenforced.
Now matches a line-leading bolded Verdict however the emphasis falls, still
anchored so prose mentioning the word does not drag an ordinary reply in.
2. _find_gate ranked $ATTEST_GATE last, behind three default paths. An
override that loses to a default is not an override: a control run pointing
it at a stand-in silently exercised the installed gate and reported on that
instead. This is the failure the skill's own non-negotiable 9 describes —
an instrument reporting the instruction it was given rather than the effect
it had — and it was in the resolver for the gate itself.
Verified by substituting a stand-in gate whose exit code is controlled, with
the hook copied to a directory with no sibling scripts/ (as gate-controls.sh
does, for exactly this reason):
before after
stand-in exits 0 ALLOWED ALLOWED
stand-in exits 1 ALLOWED BLOCKED
gate path nonexistent ALLOWED BLOCKED
gate-controls.sh: all eleven arms behave, including the three negative arms
that catch a gate which has started blocking everything.
…ntmatter mean something Three controls that were present and inert. 1. evidence-run.yml's determinism check emitted ::warning:: and carried continue-on-error, so the step whose own message reads "do not publish these numbers" went green and the numbers published. Now ::error:: plus exit 1, with continue-on-error removed from that step only. The two runner steps keep it, because there the exit code is the verdict and a finding is not a failure. Here a difference means the instrument did not return the same answer twice, so neither answer is publishable. Verified by extracting the step and running it: identical arms exit 0 — including the label/log/logs/env fields it deletes by design, which is the false positive that taught operators to publish through it — and differing arms exit 1. 2. selector-recompute.sh returned 0 for every outcome including "VALUE UNSTABLE" and "probe-failed", so a caller gating on the exit code saw green on a run whose own artifact says the number is not meaningful. The only thing between that and publication was attest-gate happening to grep the verdict string out of the prose. Now 4 for VALUE UNSTABLE and 5 for probe-failed, with 0 kept for both real measurements — a selector that recomputes is a result, not an error. Codes documented in the header, and the --help range extended so it shows them. 3. Three of fifty skill.md frontmatters were invalid YAML: `description` was a plain scalar containing ": ", which YAML reads as a nested mapping. Any tool parsing source frontmatter fails on them; tools/install masked it by folding the value to a block scalar on the way out, so the installed copy parsed and the source did not. Folds evidence and attest to block scalars at rest. The performance skill has the same defect but lives on main, so it is fixed in the tools/install branch rather than here. Round-trip checked: the installed description is byte-identical to the source description after folding. `node --test test/*.test.mjs` 61/61.
… a claim Broadening the artifact trigger made it fire on text ABOUT the trigger. A pull request quoting `**Verdict:** proven` to show what the gate matches was classified as a validation run and asked for the whole envelope, so documenting the rule became a violation of it. Fenced blocks are stripped before the trigger is tested. real verdict in prose -> artifact real verdict, bold-wrapped -> artifact verdict ONLY inside a fence -> not an artifact fenced example alongside a real one -> artifact ordinary reply -> not an artifact explicit VALIDATION_RUN marker -> artifact gate-controls.sh 11/11; chaining controls still verified; fabricated-artifact controls still 3/3 blocked. Surfaced by the trigger now working: a real published body is blocked by the CI-restatement rule, which had never run against it because the old trigger did not match its verdict line. That rule and this package's own direction disagree, and the conflict is left for the owner rather than resolved here — see the pull request description.
The proposal was to cut the prose: every principle with a hook class becomes one line pointing at the class, leaving only irreducible judgement in text. That assumes the class exists and fires, and a check NAMED after a principle is not the same as a check that catches a violation of it. scripts/principle-coverage.py decides it by measurement. It builds a body violating one principle and nothing else, publishes it past the gate, and reports whether the gate blocked it. The clean baseline is a positive control: if it does not pass, every result is uninterpretable and the script says so — the first version of this measurement reported 9/9 enforced on a baseline that was itself blocked. Result: 3 of 9 enforced, 6 exist only as prose, including items 7, 8 and 9, which are the three most recently learned. Cutting those would delete the only place the rule exists, so the cut is not made. Item 2 is the subtle one and is recorded as such: check 8 keys on a provenance marker being present anywhere in the body, so a verdict reached by reading passes as long as an artifact sits nearby. The check is a proxy for the rule. What the measurement is actually good for is the inverse of the proposal — it names the six checks worth building, in priority order, and it can be re-run after each one to show the number move. `node --test` 61/61; gate-controls 11/11; the output quoted in skill.md is reproduced by running the script.
The rule is that a validation surface does not restate CI status: "tests are green at head <sha>" hands the reviewer their own Checks tab back and carries no information. The implementation matched a bare `actions/runs/N`, which is a different thing — five of its six branches described a CLAIM about CI, and one described a URL. So a link to a specific run and job whose log holds the figure being reported was refused, and that is precisely what evidence-run.yml exists to produce: "move the measurement to CI, where the run URL is the capture". The package forbade its own flagship output, and a real published body was blocked by it. A run link is now a violation only when it carries restatement language with it. The distinction is whether the sentence asserts a status the Checks tab already shows, or points at an execution whose output the Checks tab does not. scripts/ci-citation-controls.py holds the line, with five restatements that must be caught and three citations that must be allowed. It imports CI_RESTATEMENT from the hook rather than restating it, because a control that tests its own copy of a pattern passes forever while the real one drifts. before: 3 of 8 misclassified — every citation a false positive after: 0 of 8 the same controls against the old pattern still catch the citations, so the control has power rather than passing by construction gate-controls 11/11; replaying real published bodies now shows zero regressions, where the CI rule previously blocked one; principle-coverage unchanged at 3/9; `node --test` 61/61.
…a claim The fence strip already drew this line one level up — a verdict inside a fenced block is an example of one. Inline code is the same distinction and was not covered, so a body quoting the trigger forms to document what they match was classified as making the claim. Documenting the rule violated it, found on the description of the pull request that ships the rule. Positional rather than a strip of the spans, deliberately. `_has_credible_artifact` reads paths and test refs *out of* inline code, so removing it would delete the evidence that excuses a verdict and tighten the gate instead of correcting it. `_negated`'s 16-character lookback is left alone, and now says why. It is narrow enough to miss a negator governing a subordinate clause — "cannot tell whether a screenshot shows" puts it 22 back — but a window wide enough to reach that also lets a negator elsewhere in the sentence excuse a real claim. A gate that wrongly allows an unbacked verdict fails at its purpose; one that wrongly blocks costs a rewrite. The asymmetry decides it. gate-controls.sh: 11/11 arms unchanged, so this does not loosen the gate.
Adds
evidencetopr-workflow: an orchestrator, six runners, a CI workflow that executes them, a publish gate, and an optional emit-time hook.A reviewer takes the author's tested locally, works on trust. Capture skills replay a flow and photograph the screen — but a clean screenshot is also what a silently-swallowed failure produces, and
pr-readiness-checkproves tests are present, not that behaviour is correct.evidencenames the observation that would prove a claim false, goes looking for it, and publishes what it finds where review happens.The rule the skill enforces: cite what exists, capture what you ran
Terminal text pasted into a comment is indistinguishable from terminal text invented for it. Both are plaintext; both carry the author's assertion rather than a measurement. A runner emits clean stdout, clean stdout formats into a fenced block, and a fenced block looks like evidence — so a gate that checks the text can never separate them.
The reader has to be able to check a measurement without going through the author. That requires a different medium, not better wording. The one exception is line-level citation to something externally verifiable: a permalink resolves, so an audit built from permalinks is checkable even though it is prose.
Two consequences make up most of the diff.
The measurement runs in CI.
assets/evidence-run.ymlexecutes any runner against any target repository at a pinned SHA. A local run's only witness is its author, andactions/runs/<id>is itself the artifact. Every failure class this suite has shipped was local-environment — a helper script in/tmp, a probe deleted after the run, an absolute path, a contended host producing numbers that had to be retracted — and none is expressible in CI. The workflow lives in one repository and measures others, so nothing needs installing in the repo under review.Six runners write the artifact, so nothing is retyped.
falsify-probe.shselector-recompute.shrender-count.shtsc-substitution.shcapture.shattest-gate.shEach emits JSON, a log and a markdown block, and each states in its own footer whether a reader can check it independently. Run locally, that footer reads "no reader-verifiable capture" — the artifact discloses its own weakness rather than leaving it for a gate to catch.
A runner's exit code is its verdict, so it distinguishes outcomes rather than reporting success for all of them:
selector-recompute.shreturns4forVALUE UNSTABLEand5forprobe-failed, keeping0for both real measurements — a selector that recomputes is a result, not an error. A single0across every outcome lets a caller gate on the exit code and go green on a run whose own artifact says the number is not meaningful.The publish gate
attest-gate.shruns eleven checks:marker pair,canonical header,verdict line,environment pinned,captured artifact,no prescriptions,no process narration,verdict is earned,verdict matches artifact,floats something for review,disclaimer present and early.Check 5 is the one that matters, and presence is not the test. A URL-shaped string proves nothing when the same generator writes both the claim and the string that satisfies it. An artifact counts only if the author could not have authored its bytes: a namespace where CI, the upload endpoint, or an observability backend writes them, or a local path that is actually on disk.
github.comis author-writable in general, so only/actions/runs/,/user-attachments/,/blob/,/commit/and/pull/under it qualify. It also rejects three forgeries that pass a naive version: a placeholder in a shown command, a local path in a shown command, and an A/B pair rendered as one command line printed twice with different output.Replaying real published bodies surfaced the case worth naming: evidence hosted in an author-controlled S3 bucket is refused. Fetching it unauthenticated proves it is fetchable, not that the author did not write it.
EVIDENCE_GATE_ARTIFACT_HOSTSregisters such hosts explicitly, so the downgrade from independent to merely fetchable is a choice someone made rather than a property of a regex.This is still not resolution — nothing fetches the URL and checks for a 200. Presence stays necessary and stops being sufficient.
$ATTEST_GATEis consulted before the default paths, not after. An override that loses to a default is not an override: a control run pointing it at a stand-in would silently exercise the installed gate and report on that instead — an instrument reporting the instruction it was given rather than the effect it had, which is the failure non-negotiable 9 describes, sitting in the resolver for the gate itself.Nine non-negotiables, in
skill.mdrather than a referenceA requirement you have to fetch is advisory. Four of these were added by runs that failed while producing evidence with the earlier version:
$SHA^..$SHAon a twenty-six-commit branch is a twenty-sixth of the change and looks exactly like a finished measurement — same runner, same green run, same artifactawk -vassignment escape-processed a replacement, wrote a different line than requested, failed a different test than targeted, ran the full suite, and reported a falsification for a mechanism it never touchedThe other five cover the artifact requirement,
provenrequiring execution, the absence of a "what would close it" section, writing to the reviewer rather than the commissioner, and routing privacy and security findings to the private tracker instead of publishing them.The evidence catalog — 41 lanes across 7 families
The taxonomy lives in
references/evidence-catalog.md(commit-pinned, 34 KB). Each family answers a different kind of question, so the family follows from what the claim asserts.A1 visual_validation— before/after of a reachable surfaceB3 falsifying test— fails on base, passes on branchC4 render & selector proof— WDYR counts,.recomputations()D3 LavaMoat policy diff— newly granted capabilitiesE1 Sentry links— before/after, in situF1 state migration—changedKeys, old→new stateG5 CI-workflow change— run on a test forkBolded lanes carry a claim on their own; the rest corroborate. Five lanes have their own engine skills, each in its own pull request:
B3→red-on-base(#106),B7→race-condition-repro(#97),C4→react-render-delta(#43),C9→memory-leak(#81),D3→supply-chain-auditandlavamoat-policy(#83).A known gap: every family covers product behaviour. None covers agent behaviour — whether a skill improves what an agent produces. Picking a lane cannot answer "does this guidance work", which is why reviewing skills PRs is a different problem from reviewing code.
hooks/pr-evidence-gate.pyA
PreToolUse:Bashhook enforcing the trustworthiness gate at emit time, blocking an outward-facing write whose body carries an unbacked verdict, an untracked deferral, a CI restatement, a bare or truncated identifier, a mutable/blob/<branch>/ref, a raw dump standing in as the resolver, a link-only or data-only exhibit, or a step waiver.gh apibody writes, not just the porcelain (-f body=,-F body=@file,--input). A PATCH to a comment is the same publish with a different spelling; a porcelain-only matcher is a hole, not a gate.--body-file $S/comment.md— or a body spliced in with--body "$(cat f)"yields nothing. Treating that as "nothing to scan" inverts the situation: the risk is unverifiable, not absent. An entire session of publishes went ungated that way, including one the gate blocks when handed the same body by literal path.**Verdict:** <value>and**Verdict: <value>**render identically, and matching only the first left the second silently unenforced. A verdict inside a fenced code block is a quotation, not a claim: broadening the trigger made it fire on text about the trigger, so documenting the rule became a violation of it.references/evidence-gate-setup.md.evidence-trustworthiness.mdstates which of its nineteen items the hook enforces mechanically and which stay reader-applied: it sees vocabulary, not semantics, and cannot tell what a capture actually depicts.hooks/session-audit.mjs, wired as aStophook, reports at session end when a publish went out ungated. It reports ungated publishes only.skill-auditalso emits unchained — publishes where the gate is not part of the same shell command — but the gate is aPreToolUsehook and fires out-of-band on every write by construction, so every publish is unchained and that count measures the enforcement mechanism rather than any defect.Controls
The checks above are stated as claims, so each carries the run that would falsify it.
Artifact credibility, claim and artifact in the same block (the checks are unit-scoped, so proximity matters):
https://example.invalid/nope/capture.png`src/totally/made-up.test.ts:42``evidence/never-captured.png`Fabricated 1/3 → 3/3 blocked; genuine 2/2 → 2/2 allowed.
Gate resolution, using a stand-in gate whose exit code is controlled, with the hook copied to a directory carrying no sibling
scripts/(asgate-controls.shdoes, for exactly this reason):gate-controls.sh: all eleven arms behave, including the three negative arms that catch a gate which has started blocking everything.Determinism.
evidence-run.yml's determinism step emits::error::and exits 1, withcontinue-on-errorremoved from that step only. The two runner steps keep it, because there the exit code is the verdict and a finding is not a failure; here a difference means the instrument did not return the same answer twice, so neither answer is publishable. Verified by extracting the step and running it: identical arms exit 0 — including thelabel/log/logs/envfields it deletes by design, which is the false positive that teaches operators to publish through such a step — and differing arms exit 1.session-audit.mjs's own first version treatedskill-audit's exit 1 as an error, when the exit code is the verdict — so it bailed on exactly the sessions it exists to report on, and would have read as clean forever. Caught by the positive control.node --test test/*.test.mjs— 61/61.Showcase — the falsifying-test discipline applied in this repo
Two PRs here were validated with it, so the output is checkable without leaving
MetaMask/skills.#94 — deliver knowledge a skill cites from another domain. Claim: four skills cite
testing-layers.md, which lives in another domain, so the reference cannot resolve for any consumer. Tests written against that claim, run at both refs:#87 — guard that installed knowledge references resolve. A test failing on base for the wrong reason proves nothing — an import error and a real assertion exit identically — so the base failure is read, not counted:
The message names the defect rather than a missing fixture, and the run reports 0 import or fixture errors.
What it caught that a green suite did not. A description-budget check added during this work passed while asserting nothing: the regex anchored with
^and no multiline flag, so it matched zero files. Addingmthen made$match at every line end, catching the plain frontmatter form and silently missing the folded>-form — most of the corpus. Both passes looked identical in the runner. Only running the check against a known violation exposed it.Validation runs
Trial runs against merged
metamask-extensionPRs nobody flagged. Every claim was re-checked against the real diff before posting. Clean results are included on purpose — a skill that only ever reports problems cannot be calibrated.Each comment carries a trial-run disclaimer and links back here for feedback.
Reviewing this
hooks/besideskill.mdis rejected by the structural linter untilhooksis inBUNDLE_DIRS, which #99 addsdomains/pr-workflow/skills/evidence/—skill.md, ninereferences/, sixscripts/,assets/evidence-run.yml,hooks/pr-evidence-gate.py,hooks/session-audit.mjs. Experimental, GPG-signedpr-workflowvstesting), and AEP scope — the AEP repo is internal, so the skill points at its README for stack bring-up rather than reproducing setup hereevidenceis the heaviest skill in the catalogue at ~6,800 tokens once selected. The AEP run procedure sits behindreferences/aep-local-run.mdrather than inline, since most validations use a lighter laneevidenceandattestfrontmatter is folded to block scalars:descriptionwas a plain YAML scalar containing": ", so it did not parse, andtools/installmasked it by folding on the way out — the installed copy parsed and the source did not. Theperformanceskill has the same defect but lives onmain, so it is fixed in #111. The folding has to preserve the description exactly, since the installer reads it back out:Prints
True.This branch alone does not work
The installer resolves one source directory. It does not follow
## Relatedas a dependency edge, does not fetch other refs, and reports a dangling reference as a warning rather than an error — because forward references across open pull requests are expected. So a checkout of this branch installs only the skills on it, with every cross-branch reference dead.To get a tree that actually runs:
Merges clean onto
main. Yields all elevenpr-workflowskills plusevidence, and lints at 0 errors. Three## Relatednames still dangle —react-render-delta,race-condition-repro,lavamoat-policy— because those engines ship in #43, #97 and #83.Worth knowing that the combination is also where defects surface: an error caught only with #99 and this set together — a description advertising a command the installer does not emit — passes on every branch individually.
Once the trigger actually fires,
attest-gateruns against bodies it never saw before. One real, already-published body is blocked:That body cites a CI run as the measurement, not as "tests are green at head". And two rules in this package disagree about whether that is allowed:
evidence-run.ymlexists precisely to move measurement into CI — "the run URL is the capture" — and its whole design is that a local run is self-attested and a CI run is not.attest-gatecheck 6 says a validation surface carries zero CI references.Under the second rule, evidence produced by the first rule cannot be published. That was invisible while the trigger was broken, because the check never ran.
I have deliberately not resolved this by weakening either rule — the distinction between "CI ran my measurement, here is the run" and "CI is green, therefore correct" is real, and where to draw it belongs to whoever owns check 6. Flagging rather than closing, per non-negotiable 10.
Draft: these files are pending removal
The scripts, dispatcher and probe on this branch have moved to a standalone runners repository (
v0.1.0). That repository is the source; the copies here are temporary. They remain only so this branch can be run and tested on its own while the extracted repository is reviewed.Once that review lands, they come out and the skill references the runners by pinned tag.
Why the interim needs watching. Two copies of these files is the arrangement that already failed: CI executed the runners from a branch by ref, four fixes landed on the copy it was reading, and none reached the copy under review — including a mutation runner that rewrote the line it was asked to apply, reported the full suite as having run, and published a falsification for a mechanism it never touched. That fix is cited in this description and was, until it was reconciled, still live in the code this branch ships.
So while both copies exist:
Compares the eight files that must agree and names the one that must not —
evidence-run.ymltargets a flat layout there and a nested one here. ReportsDRIFTEDwith a diff, or8 file(s) agree. Currently agreeing.What is still open, and it is layout rather than content.
hooks/pr-evidence-gate.pyis a Claude Code hook rather than a runner, so it fits neither repository cleanly and may end up in a third place. The skill text, the catalog, the non-negotiables and the showcase do not depend on any of that — they are reviewable now, and comments on them are wanted.